using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using IWMAG.Scripting.Interpreter;
using System.IO;
using IWMAG.Core.Helpers;
using IWMAG.Scripting.Compiler;
using System.Reflection;
using IWMAG.Core.Objects;
using IWMAG.Core.Messaging;
using IWMAG.Core.Hitboxes;
using System.Threading;
using System.Threading.Tasks;

namespace IWMAG.Engine {
    public partial class GameEngine : Microsoft.Xna.Framework.Game {
        /// <summary>
        /// When the script has been running for this much time, a warning will be given in the console.
        /// </summary>
        const int ScriptWarningTime = 50;
        /// <summary>
        /// When the script has been running for this much time, the script will be forcibly terminated.
        /// </summary>
        const int ScriptKillTime = 1000;

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        //TODO: Make this a property if its public
        public ScriptEngine Script = new ScriptEngine();
        SpriteFont font;
        //TODO: Make this a property if its protected
        protected GameConsole Log = new GameConsole();
        Texture2D Pixel;
        string errors;
        KeyboardState lastState;
        bool showConsole = true;
        //TODO: Make this a property if its public
        public Level CurrentLevel;
        //TODO: Make this a property if its public
        public bool Paused = false;

        public GameEngine() {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize() {
            Script.ExposedAssemblies.Add(Assembly.GetExecutingAssembly());
            Script.ExposedAssemblies.Add(Assembly.GetAssembly(typeof(Color)));

            Script.Code = File.ReadAllText("Content/Scripts/boot.gb");
            Script.Introduce("gravity", new Vector2(0, 200));
            Script.ImportType("color", typeof(Color));
            Script.ImportType("vector", typeof(Vector2));
            Script.ImportType("character", typeof(Character));
            Script.ImportType("rect", typeof(Rectangle));
            Script.Introduce("zones", new List<Rectangle>());

            Script.Introduce("player", new Character());
            //script.Introduce("game", this);
            LinkApi();

            CurrentLevel = new Level(Script);

            errors = String.Join("\n", (IEnumerable<IUserInfo>)Script.Compile());

            Script.Writer = Log.Writer;

            if (Script.Compiled) {
                Script.Start("boot");

                Character c = new Character();
                c.Initialize(Content);
                CurrentLevel.Add(c);
                Script.Update(0);
                Script.SetValue("player", c);
            }

            script_runner = new Task(() => UpdateScriptBackground(), script_cancellation_token.Token,TaskCreationOptions.LongRunning);
            script_runner.Start();

            base.Initialize();
        }

        protected override void LoadContent() {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            font = Content.Load<SpriteFont>("Font");
            Pixel = Content.Load<Texture2D>("pixel");

            
        }

        protected override void UnloadContent() {
        }

        protected override void Update(GameTime gameTime) {
            if (Paused) {
                return;
            }

            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
            KeyboardState ks = Keyboard.GetState();
            if (ks.IsKeyDown(Keys.Escape)) {
                this.Exit();
            }
            if (ks.IsKeyDown(Keys.OemTilde) && lastState.IsKeyUp(Keys.OemTilde)) {
                showConsole = !showConsole;
            }

            if (ks != lastState) {
                CurrentLevel.Messages.SendMessage(new Message(EventType.OnInput, ks));
            }

            CurrentLevel.Update(elapsed);
            lastState = ks;
            Log.Update(elapsed);
            UpdateScript(elapsed);

            base.Update(gameTime);
        }


        Task script_runner;
        CancellationTokenSource script_cancellation_token = new CancellationTokenSource();
        float bg_elapsed;
        ManualResetEventSlim mres_start = new ManualResetEventSlim(false);
        ManualResetEventSlim mres_stop = new ManualResetEventSlim(false);
        bool scriptTimedOut = false;

        bool isCurrentlyKilling = false;
        ScriptTask scriptThreadToKill;

        void UpdateScript(float elapsed) {
            if (scriptTimedOut) {
                return;
            }
            bg_elapsed = elapsed;
            mres_stop.Reset();
            mres_start.Set();
            if (!mres_stop.Wait(ScriptWarningTime) && !Script.IsExterned) {
                Log.Writer.WriteLine("Function `{0}` might be in an infinite loop", Script.ExecutingThread.Name);
                if (!isCurrentlyKilling) {
                    isCurrentlyKilling = true;
                    scriptThreadToKill = Script.ExecutingThread;
                    Script.Kill();
                    return;
                }

                //Script.Stop(scriptThreadToKill);
                
                // TODO: get rid of this false
                if (false && !mres_stop.Wait(ScriptKillTime - ScriptWarningTime)) {
                    Log.Writer.WriteLine("Function `{0}` not responding.", Script.ExecutingThread.Name);

                    script_cancellation_token.Cancel(true);

                    if (script_runner.Wait(200)) {
                        Log.Writer.WriteLine("Script died gracefully!");
                    } else {
                        Log.Writer.WriteLine("Script is still struggling to hold on to life...");
                        script_runner_thread.Abort();
                        try
                        {
                            script_runner.Wait(-1);
                        }
                        catch (AggregateException) { }     
                    }

                    scriptTimedOut = true;

                    Log.Writer.WriteLine(script_runner.Status.ToString());
                }

            }
           
        }

        Thread script_runner_thread;

        void UpdateScriptBackground() {
            bool keepGoing = true;
            script_runner_thread = Thread.CurrentThread;
            script_cancellation_token.Token.ThrowIfCancellationRequested();
            try {
                while (keepGoing) {
                    mres_start.Wait(script_cancellation_token.Token);
                    mres_start.Reset();
                    Script.Update(bg_elapsed);
                    mres_stop.Set();
                }
            } catch (OperationCanceledException) {
                return;
            }
        }

        protected override void Draw(GameTime gameTime) {
            float elapsed = (float) gameTime.ElapsedGameTime.TotalSeconds;

            GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();
            if (Script != null && Script.Compiled) {
                foreach (Rectangle r in Script.GetArray<Rectangle>("zones")) {
                    spriteBatch.Draw(Pixel, r, new Color(255, 185, 40));
                }
            }

            CurrentLevel.Draw(elapsed, spriteBatch);

            if (showConsole) {
                spriteBatch.Draw(Pixel, new Rectangle(0, 0, 400, 168), new Color(new Vector4(0, 0, 0, 0.5f)));

                if (Script != null && Script.Compiled) {
                    spriteBatch.DrawString(font, Log.Text, new Vector2(8), Color.White);
                } else {
                    spriteBatch.DrawString(font, errors, new Vector2(8), Color.White);
                }
            }

            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}